package com.stars.easyms.swagger.plugin;

import com.fasterxml.classmate.ResolvedType;
import com.google.common.base.Optional;
import com.stars.easyms.rest.annotation.EasyMsRestMapping;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.schema.JacksonEnumTypeDeterminer;
import springfox.documentation.schema.TypeNameExtractor;
import springfox.documentation.service.Parameter;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.EnumTypeDeterminer;
import springfox.documentation.spi.schema.contexts.ModelContext;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.ParameterContext;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;

import static java.lang.String.format;
import static springfox.documentation.schema.Collections.isContainerType;
import static springfox.documentation.schema.Maps.isMapType;
import static springfox.documentation.schema.ResolvedTypes.modelRefFactory;
import static springfox.documentation.schema.Types.isBaseType;
import static springfox.documentation.schema.Types.typeNameFor;
import static springfox.documentation.spi.schema.contexts.ModelContext.inputParam;

/**
 * <p>className: EasyMsOperationParameterReader</p>
 * <p>description: EasyMs的swagger模块的操作参数的reader类</p>
 *
 * @author guoguifang
 * @version 1.2.2
 * @date 2019-07-30 13:31
 */
public final class EasyMsOperationParameterReader implements OperationBuilderPlugin {

    private EnumTypeDeterminer enumTypeDeterminer;

    private TypeNameExtractor nameExtractor;

    public EasyMsOperationParameterReader(EnumTypeDeterminer enumTypeDeterminer, TypeNameExtractor nameExtractor) {
        this.enumTypeDeterminer = enumTypeDeterminer;
        this.nameExtractor = nameExtractor;
    }

    @Override
    public void apply(OperationContext context) {
        if (context.findAnnotation(EasyMsRestMapping.class).isPresent()) {
            List<ResolvedMethodParameter> methodParameters = context.getParameters();
            if (methodParameters.size() != 1) {
                return;
            }
            ResolvedMethodParameter methodParameter = methodParameters.get(0);
            ResolvedType alternate = context.alternateFor(methodParameter.getParameterType());
            if (!shouldIgnore(methodParameter, alternate, context.getIgnorableParameterTypes())) {
                ParameterContext parameterContext = new ParameterContext(methodParameter, new ParameterBuilder(),
                        context.getDocumentationContext(), context.getGenericsNamingStrategy(), context);
                if (shouldExpand(methodParameter, alternate) && nameExtractor != null) {
                    Optional<String> discoveredName = parameterContext.resolvedMethodParameter().defaultName();
                    String name = discoveredName.isPresent() ? discoveredName.get()
                            : format("param%s", parameterContext.resolvedMethodParameter().getParameterIndex());
                    ModelContext modelContext = inputParam(
                            parameterContext.getGroupName(), alternate,
                            parameterContext.getDocumentationType(),
                            parameterContext.getAlternateTypeProvider(),
                            parameterContext.getGenericNamingStrategy(),
                            parameterContext.getIgnorableParameterTypes());
                    parameterContext.parameterBuilder().allowMultiple(false).parameterType("body").required(true).type(alternate)
                            .modelRef(modelRefFactory(modelContext, nameExtractor).apply(alternate))
                            .defaultValue(null).name(name).description(name).allowableValues(null);

                    OperationBuilder operationBuilder = context.operationBuilder();
                    Field field = FieldUtils.getDeclaredField(OperationBuilder.class, "parameters", true);
                    if (field != null) {
                        try {
                            List<Parameter> parameters = (List<Parameter>) field.get(operationBuilder);
                            parameters.clear();
                        } catch (IllegalAccessException e) {
                            // ignore
                        }
                    }
                    operationBuilder.parameters(Collections.singletonList(parameterContext.parameterBuilder().build()));
                }
            }
        }
    }

    @Override
    public boolean supports(DocumentationType delimiter) {
        return true;
    }

    private boolean shouldIgnore(final ResolvedMethodParameter parameter, ResolvedType resolvedParameterType, final Set<Class> ignorableParamTypes) {
        if (ignorableParamTypes.contains(resolvedParameterType.getErasedType())) {
            return true;
        }
        return ignorableParamTypes.stream().filter(Annotation.class::isAssignableFrom).anyMatch(parameter::hasParameterAnnotation);
    }

    private boolean shouldExpand(final ResolvedMethodParameter parameter, ResolvedType resolvedParamType) {
        return !parameter.hasParameterAnnotation(RequestBody.class)
                && !parameter.hasParameterAnnotation(RequestPart.class)
                && !parameter.hasParameterAnnotation(RequestParam.class)
                && !parameter.hasParameterAnnotation(PathVariable.class)
                && !isBaseType(typeNameFor(resolvedParamType.getErasedType()))
                && !getEnumTypeDeterminer().isEnum(resolvedParamType.getErasedType())
                && !isContainerType(resolvedParamType)
                && !isMapType(resolvedParamType);

    }

    private EnumTypeDeterminer getEnumTypeDeterminer() {
        if (enumTypeDeterminer == null) {
            enumTypeDeterminer = new JacksonEnumTypeDeterminer();
        }
        return enumTypeDeterminer;
    }

}